/*
 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.security.auth;

import java.util.*;
import java.text.MessageFormat;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Principal;
import sun.security.util.ResourcesMgr;

/**
 * This class is used to protect access to private Credentials
 * belonging to a particular {@code Subject}.  The {@code Subject}
 * is represented by a Set of Principals.
 *
 * <p> The target name of this {@code Permission} specifies
 * a Credential class name, and a Set of Principals.
 * The only valid value for this Permission's actions is, "read".
 * The target name must abide by the following syntax:
 *
 * <pre>
 *      CredentialClass {PrincipalClass "PrincipalName"}*
 * </pre>
 *
 * For example, the following permission grants access to the
 * com.sun.PrivateCredential owned by Subjects which have
 * a com.sun.Principal with the name, "duke".  Note that although
 * this example, as well as all the examples below, do not contain
 * Codebase, SignedBy, or Principal information in the grant statement
 * (for simplicity reasons), actual policy configurations should
 * specify that information when appropriate.
 *
 * <pre>
 *
 *    grant {
 *      permission javax.security.auth.PrivateCredentialPermission
 *              "com.sun.PrivateCredential com.sun.Principal \"duke\"",
 *              "read";
 *    };
 * </pre>
 *
 * If CredentialClass is "*", then access is granted to
 * all private Credentials belonging to the specified
 * {@code Subject}.
 * If "PrincipalName" is "*", then access is granted to the
 * specified Credential owned by any {@code Subject} that has the
 * specified {@code Principal} (the actual PrincipalName doesn't matter).
 * For example, the following grants access to the
 * a.b.Credential owned by any {@code Subject} that has
 * an a.b.Principal.
 *
 * <pre>
 *    grant {
 *      permission javax.security.auth.PrivateCredentialPermission
 *              "a.b.Credential a.b.Principal "*"",
 *              "read";
 *    };
 * </pre>
 *
 * If both the PrincipalClass and "PrincipalName" are "*",
 * then access is granted to the specified Credential owned by
 * any {@code Subject}.
 *
 * <p> In addition, the PrincipalClass/PrincipalName pairing may be repeated:
 *
 * <pre>
 *    grant {
 *      permission javax.security.auth.PrivateCredentialPermission
 *              "a.b.Credential a.b.Principal "duke" c.d.Principal "dukette"",
 *              "read";
 *    };
 * </pre>
 *
 * The above grants access to the private Credential, "a.b.Credential",
 * belonging to a {@code Subject} with at least two associated Principals:
 * "a.b.Principal" with the name, "duke", and "c.d.Principal", with the name,
 * "dukette".
 */
public final class PrivateCredentialPermission extends Permission {

  private static final long serialVersionUID = 5284372143517237068L;

  private static final CredOwner[] EMPTY_PRINCIPALS = new CredOwner[0];

  /**
   * @serial
   */
  private String credentialClass;

  /**
   * @serial The Principals associated with this permission. The set contains elements of type,
   * {@code PrivateCredentialPermission.CredOwner}.
   */
  private Set<Principal> principals;  // ignored - kept around for compatibility
  private transient CredOwner[] credOwners;

  /**
   * @serial
   */
  private boolean testing = false;

  /**
   * Create a new {@code PrivateCredentialPermission}
   * with the specified {@code credentialClass} and Principals.
   */
  PrivateCredentialPermission(String credentialClass,
      Set<Principal> principals) {

    super(credentialClass);
    this.credentialClass = credentialClass;

    synchronized (principals) {
      if (principals.size() == 0) {
        this.credOwners = EMPTY_PRINCIPALS;
      } else {
        this.credOwners = new CredOwner[principals.size()];
        int index = 0;
        Iterator<Principal> i = principals.iterator();
        while (i.hasNext()) {
          Principal p = i.next();
          this.credOwners[index++] = new CredOwner
              (p.getClass().getName(),
                  p.getName());
        }
      }
    }
  }

  /**
   * Creates a new {@code PrivateCredentialPermission}
   * with the specified {@code name}.  The {@code name}
   * specifies both a Credential class and a {@code Principal} Set.
   *
   * <p>
   *
   * @param name the name specifying the Credential class and {@code Principal} Set. <p>
   * @param actions the actions specifying that the Credential can be read.
   * @throws IllegalArgumentException if {@code name} does not conform to the correct syntax or if
   * {@code actions} is not "read".
   */
  public PrivateCredentialPermission(String name, String actions) {
    super(name);

    if (!"read".equalsIgnoreCase(actions)) {
      throw new IllegalArgumentException
          (ResourcesMgr.getString("actions.can.only.be.read."));
    }
    init(name);
  }

  /**
   * Returns the Class name of the Credential associated with this
   * {@code PrivateCredentialPermission}.
   *
   * <p>
   *
   * @return the Class name of the Credential associated with this {@code
   * PrivateCredentialPermission}.
   */
  public String getCredentialClass() {
    return credentialClass;
  }

  /**
   * Returns the {@code Principal} classes and names
   * associated with this {@code PrivateCredentialPermission}.
   * The information is returned as a two-dimensional array (array[x][y]).
   * The 'x' value corresponds to the number of {@code Principal}
   * class and name pairs.  When (y==0), it corresponds to
   * the {@code Principal} class value, and when (y==1),
   * it corresponds to the {@code Principal} name value.
   * For example, array[0][0] corresponds to the class name of
   * the first {@code Principal} in the array.  array[0][1]
   * corresponds to the {@code Principal} name of the
   * first {@code Principal} in the array.
   *
   * <p>
   *
   * @return the {@code Principal} class and names associated with this {@code
   * PrivateCredentialPermission}.
   */
  public String[][] getPrincipals() {

    if (credOwners == null || credOwners.length == 0) {
      return new String[0][0];
    }

    String[][] pArray = new String[credOwners.length][2];
    for (int i = 0; i < credOwners.length; i++) {
      pArray[i][0] = credOwners[i].principalClass;
      pArray[i][1] = credOwners[i].principalName;
    }
    return pArray;
  }

  /**
   * Checks if this {@code PrivateCredentialPermission} implies
   * the specified {@code Permission}.
   *
   * <p>
   *
   * This method returns true if:
   * <ul>
   * <li> <i>p</i> is an instanceof PrivateCredentialPermission and
   * <li> the target name for <i>p</i> is implied by this object's
   * target name.  For example:
   * <pre>
   *  [* P1 "duke"] implies [a.b.Credential P1 "duke"].
   *  [C1 P1 "duke"] implies [C1 P1 "duke" P2 "dukette"].
   *  [C1 P2 "dukette"] implies [C1 P1 "duke" P2 "dukette"].
   * </pre>
   * </ul>
   *
   * <p>
   *
   * @param p the {@code Permission} to check against.
   * @return true if this {@code PrivateCredentialPermission} implies the specified {@code
   * Permission}, false if not.
   */
  public boolean implies(Permission p) {

    if (p == null || !(p instanceof PrivateCredentialPermission)) {
      return false;
    }

    PrivateCredentialPermission that = (PrivateCredentialPermission) p;

    if (!impliesCredentialClass(credentialClass, that.credentialClass)) {
      return false;
    }

    return impliesPrincipalSet(credOwners, that.credOwners);
  }

  /**
   * Checks two {@code PrivateCredentialPermission} objects for
   * equality.  Checks that <i>obj</i> is a
   * {@code PrivateCredentialPermission},
   * and has the same credential class as this object,
   * as well as the same Principals as this object.
   * The order of the Principals in the respective Permission's
   * target names is not relevant.
   *
   * <p>
   *
   * @param obj the object we are testing for equality with this object.
   * @return true if obj is a {@code PrivateCredentialPermission}, has the same credential class as
   * this object, and has the same Principals as this object.
   */
  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }

    if (!(obj instanceof PrivateCredentialPermission)) {
      return false;
    }

    PrivateCredentialPermission that = (PrivateCredentialPermission) obj;

    return (this.implies(that) && that.implies(this));
  }

  /**
   * Returns the hash code value for this object.
   *
   * @return a hash code value for this object.
   */
  public int hashCode() {
    return this.credentialClass.hashCode();
  }

  /**
   * Returns the "canonical string representation" of the actions.
   * This method always returns the String, "read".
   *
   * <p>
   *
   * @return the actions (always returns "read").
   */
  public String getActions() {
    return "read";
  }

  /**
   * Return a homogeneous collection of PrivateCredentialPermissions
   * in a {@code PermissionCollection}.
   * No such {@code PermissionCollection} is defined,
   * so this method always returns {@code null}.
   *
   * <p>
   *
   * @return null in all cases.
   */
  public PermissionCollection newPermissionCollection() {
    return null;
  }

  private void init(String name) {

    if (name == null || name.trim().length() == 0) {
      throw new IllegalArgumentException("invalid empty name");
    }

    ArrayList<CredOwner> pList = new ArrayList<>();
    StringTokenizer tokenizer = new StringTokenizer(name, " ", true);
    String principalClass = null;
    String principalName = null;

    if (testing) {
      System.out.println("whole name = " + name);
    }

    // get the Credential Class
    credentialClass = tokenizer.nextToken();
    if (testing) {
      System.out.println("Credential Class = " + credentialClass);
    }

    if (tokenizer.hasMoreTokens() == false) {
      MessageFormat form = new MessageFormat(ResourcesMgr.getString
          ("permission.name.name.syntax.invalid."));
      Object[] source = {name};
      throw new IllegalArgumentException
          (form.format(source) + ResourcesMgr.getString
              ("Credential.Class.not.followed.by.a.Principal.Class.and.Name"));
    }

    while (tokenizer.hasMoreTokens()) {

      // skip delimiter
      tokenizer.nextToken();

      // get the Principal Class
      principalClass = tokenizer.nextToken();
      if (testing) {
        System.out.println("    Principal Class = " + principalClass);
      }

      if (tokenizer.hasMoreTokens() == false) {
        MessageFormat form = new MessageFormat(ResourcesMgr.getString
            ("permission.name.name.syntax.invalid."));
        Object[] source = {name};
        throw new IllegalArgumentException
            (form.format(source) + ResourcesMgr.getString
                ("Principal.Class.not.followed.by.a.Principal.Name"));
      }

      // skip delimiter
      tokenizer.nextToken();

      // get the Principal Name
      principalName = tokenizer.nextToken();

      if (!principalName.startsWith("\"")) {
        MessageFormat form = new MessageFormat(ResourcesMgr.getString
            ("permission.name.name.syntax.invalid."));
        Object[] source = {name};
        throw new IllegalArgumentException
            (form.format(source) + ResourcesMgr.getString
                ("Principal.Name.must.be.surrounded.by.quotes"));
      }

      if (!principalName.endsWith("\"")) {

        // we have a name with spaces in it --
        // keep parsing until we find the end quote,
        // and keep the spaces in the name

        while (tokenizer.hasMoreTokens()) {
          principalName = principalName + tokenizer.nextToken();
          if (principalName.endsWith("\"")) {
            break;
          }
        }

        if (!principalName.endsWith("\"")) {
          MessageFormat form = new MessageFormat
              (ResourcesMgr.getString
                  ("permission.name.name.syntax.invalid."));
          Object[] source = {name};
          throw new IllegalArgumentException
              (form.format(source) + ResourcesMgr.getString
                  ("Principal.Name.missing.end.quote"));
        }
      }

      if (testing) {
        System.out.println("\tprincipalName = '" + principalName + "'");
      }

      principalName = principalName.substring
          (1, principalName.length() - 1);

      if (principalClass.equals("*") &&
          !principalName.equals("*")) {
        throw new IllegalArgumentException(ResourcesMgr.getString
            ("PrivateCredentialPermission.Principal.Class.can.not.be.a.wildcard.value.if.Principal.Name.is.not.a.wildcard.value"));
      }

      if (testing) {
        System.out.println("\tprincipalName = '" + principalName + "'");
      }

      pList.add(new CredOwner(principalClass, principalName));
    }

    this.credOwners = new CredOwner[pList.size()];
    pList.toArray(this.credOwners);
  }

  private boolean impliesCredentialClass(String thisC, String thatC) {

    // this should never happen
    if (thisC == null || thatC == null) {
      return false;
    }

    if (testing) {
      System.out.println("credential class comparison: " +
          thisC + "/" + thatC);
    }

    if (thisC.equals("*")) {
      return true;
    }

    /**
     * XXX let's not enable this for now --
     *      if people want it, we'll enable it later
     */
        /*
        if (thisC.endsWith("*")) {
            String cClass = thisC.substring(0, thisC.length() - 2);
            return thatC.startsWith(cClass);
        }
        */

    return thisC.equals(thatC);
  }

  private boolean impliesPrincipalSet(CredOwner[] thisP, CredOwner[] thatP) {

    // this should never happen
    if (thisP == null || thatP == null) {
      return false;
    }

    if (thatP.length == 0) {
      return true;
    }

    if (thisP.length == 0) {
      return false;
    }

    for (int i = 0; i < thisP.length; i++) {
      boolean foundMatch = false;
      for (int j = 0; j < thatP.length; j++) {
        if (thisP[i].implies(thatP[j])) {
          foundMatch = true;
          break;
        }
      }
      if (!foundMatch) {
        return false;
      }
    }
    return true;
  }

  /**
   * Reads this object from a stream (i.e., deserializes it)
   */
  private void readObject(java.io.ObjectInputStream s) throws
      java.io.IOException,
      ClassNotFoundException {

    s.defaultReadObject();

    // perform new initialization from the permission name

    if (getName().indexOf(" ") == -1 && getName().indexOf("\"") == -1) {

      // name only has a credential class specified
      credentialClass = getName();
      credOwners = EMPTY_PRINCIPALS;

    } else {

      // perform regular initialization
      init(getName());
    }
  }

  /**
   * @serial include
   */
  static class CredOwner implements java.io.Serializable {

    private static final long serialVersionUID = -5607449830436408266L;

    /**
     * @serial
     */
    String principalClass;
    /**
     * @serial
     */
    String principalName;

    CredOwner(String principalClass, String principalName) {
      this.principalClass = principalClass;
      this.principalName = principalName;
    }

    public boolean implies(Object obj) {
      if (obj == null || !(obj instanceof CredOwner)) {
        return false;
      }

      CredOwner that = (CredOwner) obj;

      if (principalClass.equals("*") ||
          principalClass.equals(that.principalClass)) {

        if (principalName.equals("*") ||
            principalName.equals(that.principalName)) {
          return true;
        }
      }

      /**
       * XXX no code yet to support a.b.*
       */

      return false;
    }

    public String toString() {
      MessageFormat form = new MessageFormat(ResourcesMgr.getString
          ("CredOwner.Principal.Class.class.Principal.Name.name"));
      Object[] source = {principalClass, principalName};
      return (form.format(source));
    }
  }
}
